﻿// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
// -----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using Microsoft.Internal;
using System.ComponentModel.Composition.ReflectionModel;

namespace System.ComponentModel.Composition.Caching.AttributedModel
{
    internal static class ReflectionCacheServices
    {
        public static void WriteReflectionItem(this IDictionary<string, object> cache, ReflectionItem item, Type defaultType)
        {
            switch (item.ItemType)
            {
                case ReflectionItemType.Property:
                {   // Properties are special; we need to write them as two accessors
                    // as we cannot resolve properties from a metadata token

                    ReflectionProperty property = (ReflectionProperty)item;
                    cache.WriteProperty(property.UnderlyingGetMethod, property.UnderlyingSetMethod, defaultType);
                    return;
                }

                case ReflectionItemType.Method:
                case ReflectionItemType.Field:                
                case ReflectionItemType.Type:
                {
                    cache.WriteMember(((ReflectionMember)item).UnderlyingMember, defaultType);
                    return;
                }

                case ReflectionItemType.Parameter:
                default:
                {
                    Assumes.IsTrue(item.ItemType == ReflectionItemType.Parameter);
                    cache.WriteParameter(((ReflectionParameter)item).UnderlyingParameter);
                    return;
                }
            }
        }

        private static void WriteParameter(this IDictionary<string, object> cache, ParameterInfo parameter)
        {
            cache.Write(WriteParameterCore, AttributedCacheServices.CacheKeys.Parameter, parameter);
        }

        private static void WriteProperty(this IDictionary<string, object> cache, MethodInfo getMethod, MethodInfo setMethod, Type defaultType)
        {
            cache.Write((c, m) => WritePropertyCore(c, m, defaultType), AttributedCacheServices.CacheKeys.Property, new MethodInfo[] { getMethod, setMethod } );
        }

        private static void WriteMember(this IDictionary<string, object> cache, MemberInfo member, Type defaultType)
        {
            cache.WriteMember(AttributedCacheServices.CacheKeys.Member, member, defaultType);
        }

        public static void WriteMember(this IDictionary<string, object> cache, string key, MemberInfo member)
        {
            cache.WriteMember(key, member, (Type)null);
        }
        
        public static void WriteMember(this IDictionary<string, object> cache, string key, MemberInfo member, Type defaultType)
        {
            cache.Write((c, m) => WriteMemberCore(c, m, defaultType), key, member);
        }

        private static void WriteModule(this IDictionary<string, object> cache, Module module)
        {
            // We only store the module in the case where it is not the default
            cache.Write(WriteModuleCore, AttributedCacheServices.CacheKeys.Module, module, module.Assembly.ManifestModule);
        }

        public static void WriteAssembly(this IDictionary<string, object> cache, Assembly assembly)
        {
            cache.WriteAssembly(assembly, (Assembly)null);
        }

        public static void WriteAssembly(this IDictionary<string, object> cache, Assembly assembly, Assembly defaultAssembly)
        {
            cache.Write(WriteAssemblyCore, AttributedCacheServices.CacheKeys.Assembly, assembly, defaultAssembly);
        }
        
        private static ParameterInfo LoadParameter(this IDictionary<string, object> cache, ConstructorInfo constructor)
        {
            return cache.Load(c => LoadParameterCore(c, constructor), AttributedCacheServices.CacheKeys.Parameter);
        }

        private static MethodInfo[] LoadProperty(this IDictionary<string, object> cache, Type defaultType)
        {
            return cache.Load<MethodInfo[]>(c => LoadPropertyCore(c, defaultType), AttributedCacheServices.CacheKeys.Property); 
        }

        public static ReflectionMember LoadReflectionMember(this IDictionary<string, object> cache, Type defaultType)
        {
            MethodInfo[] methods = cache.LoadProperty(defaultType);
            if (methods != null)
            {   
                return new ReflectionProperty(methods[0], methods[1]);
            }

            MemberInfo member = cache.LoadMember<MemberInfo>(defaultType);
            if (member != null)
            {
                return member.ToReflectionMember();
            }
            
            return null;
        }

        public static ReflectionParameter LoadReflectionParameter(this IDictionary<string, object> cache, ConstructorInfo constructor)
        {
            ParameterInfo parameter = LoadParameter(cache, constructor);

            return new ReflectionParameter(parameter);
        }

        private static T LoadMember<T>(this IDictionary<string, object> cache, Type defaultType) where T : MemberInfo
        {
            return cache.LoadMember<T>(AttributedCacheServices.CacheKeys.Member, defaultType);
        }

        public static T LoadMember<T>(this IDictionary<string, object> cache, string key) where T : MemberInfo
        {
            return cache.LoadMember<T>(key, (Type)null);
        }

        public static T LoadMember<T>(this IDictionary<string, object> cache, string key, Type defaultType) where T : MemberInfo
        {
            return cache.Load<T>(c => LoadMemberCore<T>(c, defaultType), key);
        }

        private static Module LoadModule(this IDictionary<string, object> cache, Assembly assembly)
        {
            return cache.Load(c => LoadModuleCore(c, assembly), AttributedCacheServices.CacheKeys.Module, assembly.ManifestModule);
        }

        public static Assembly LoadAssembly(this IDictionary<string, object> cache)
        {
            return cache.LoadAssembly((Assembly)null);
        }

        public static Assembly LoadAssembly(this IDictionary<string, object> cache, Assembly defaultAssembly)
        {
            return cache.LoadAssembly(AttributedCacheServices.CacheKeys.Assembly, defaultAssembly);
        }

        private static Assembly LoadAssembly(this IDictionary<string, object> cache, string key, Assembly defaultAssembly)
        {
            return cache.Load<Assembly>(LoadAssemblyCore, key, defaultAssembly);
        }

        private static void WriteParameterCore(IDictionary<string, object> cache, ParameterInfo parameter)
        {
            Assumes.NotNull(cache, parameter);

            cache.WriteValue(AttributedCacheServices.CacheKeys.ParameterPosition, parameter.Position, -1);
        }

        private static void WritePropertyCore(IDictionary<string, object> cache, MethodInfo[] accessors, Type defaultType)
        {
            Assumes.NotNull(cache, defaultType);

            MethodInfo getMethod = accessors[0];
            MethodInfo setMethod = accessors[1];

            Assumes.IsTrue(getMethod != null || setMethod != null);

            cache.WriteMember(AttributedCacheServices.CacheKeys.PropertyGetMethod, getMethod, defaultType);
            cache.WriteMember(AttributedCacheServices.CacheKeys.PropertySetMethod, setMethod, defaultType);
        }

        private static void WriteMemberCore(IDictionary<string, object> cache, MemberInfo member, Type defaultType)
        {
            Assumes.NotNull(cache, member);

            cache.WriteValue(AttributedCacheServices.CacheKeys.MetadataToken, member.MetadataToken);
            cache.WriteModule(member.Module);
            cache.WriteAssembly(member.Module.Assembly, defaultType == null ? null : defaultType.Assembly);
        }        

        private static void WriteModuleCore(IDictionary<string, object> cache, Module module)
        {
            Assumes.NotNull(module, cache);

            cache.WriteValue<int>(AttributedCacheServices.CacheKeys.MetadataToken, module.MetadataToken);
        }

        private static void WriteAssemblyCore(IDictionary<string, object> cache, Assembly assembly)
        {
            Assumes.NotNull(assembly);
            Assumes.NotNull(cache);

#if !SILVERLIGHT
            // NOTE : we need to store the path to the assembly. We can't use Location as in some cases - such as shadow copying - 
            // a new location will be created each time the application run. The Codebase is significantly more resilient, but 
            // it has the Uri format, and we will need to extract the path. We will also check that the assembly has been loaded
            // from file, as we don't support anything else at this point.
            // To do that we check that 
            // 1. Uri is a file
            // 2. Location is not empty (it being empty indicates that the assembly has been loaded from a byte stream or in some other funky way.
            Uri assemblyCodeBaseUri = new Uri(assembly.CodeBase);
            if (!assemblyCodeBaseUri.IsFile || string.IsNullOrEmpty(assembly.Location))
            {
                throw new InvalidOperationException(string.Format( CultureInfo.InvariantCulture, Strings.AssemblyInformationCannotBeCached, assembly.FullName));
            }
            // Local path properly will give full UNC path
            cache.WriteValue(AttributedCacheServices.CacheKeys.AssemblyLocation, assemblyCodeBaseUri.LocalPath);
            cache.WriteValue(AttributedCacheServices.CacheKeys.AssemblyTimeStamp, File.GetLastWriteTimeUtc(assemblyCodeBaseUri.LocalPath).Ticks);
#endif

            cache.WriteValue(AttributedCacheServices.CacheKeys.AssemblyFullName, assembly.FullName);
        }

        public static string GetAssemblyName(this IDictionary<string, object> cache)
        {
            cache = cache.ReadDictionary<object>(AttributedCacheServices.CacheKeys.Assembly);

            return cache.ReadValue<string>(AttributedCacheServices.CacheKeys.AssemblyFullName);
        }

        public static bool IsAssemblyCacheUpToDate(this IDictionary<string, object> cache)
        {
            Assumes.NotNull(cache);

            cache = cache.ReadDictionary<object>(AttributedCacheServices.CacheKeys.Assembly);

#if !SILVERLIGHT
            string assemblyLocation = cache.ReadValue<string>(AttributedCacheServices.CacheKeys.AssemblyLocation);
            long assemblyTimeStamp = cache.ReadValue<long>(AttributedCacheServices.CacheKeys.AssemblyTimeStamp);
            if (!File.Exists(assemblyLocation))
            {
                return false;
            }
            if (File.GetLastWriteTimeUtc(assemblyLocation).Ticks != assemblyTimeStamp)
            {
                return false;
            }
#endif
            return true;
        }

        private static Assembly LoadAssemblyCore(this IDictionary<string, object> cache)
        {
            Assumes.NotNull(cache);

            // NOTE : we load assembly with the full name AND codebase
            // This will force the loader to look for the default probing path - local directories, GAC, etc - and only after that fallback onto the path
            // if we want the reverse behavior, we will need to try loading from codebase first, and of that fails load from the defualt context
            string assemblyName = cache.ReadValue<string>(AttributedCacheServices.CacheKeys.AssemblyFullName);
            string codeBase = cache.ReadValue<string>(AttributedCacheServices.CacheKeys.AssemblyLocation);

            return ReflectionResolver.ResolveAssembly(assemblyName, codeBase);
        }

        private static Module LoadModuleCore(this IDictionary<string, object> cache, Assembly assembly)
        {
            Assumes.NotNull(cache, assembly);

            int metadataToken = cache.ReadValue<int>(AttributedCacheServices.CacheKeys.MetadataToken);

            return ReflectionResolver.ResolveModule(assembly, metadataToken);
        }

        private static MethodInfo[] LoadPropertyCore(IDictionary<string, object> cache, Type defaultType)
        {
            Assumes.NotNull(cache);

            MethodInfo getMethod = LoadMember<MethodInfo>(cache, AttributedCacheServices.CacheKeys.PropertyGetMethod, defaultType);
            MethodInfo setMethod = LoadMember<MethodInfo>(cache, AttributedCacheServices.CacheKeys.PropertySetMethod, defaultType);

            if (getMethod != null || setMethod != null)
            {   
                // Return the array if we have either the get or set method
                return new MethodInfo[] { getMethod, setMethod };
            }

            return null;
        }

        private static T LoadMemberCore<T>(IDictionary<string, object> cache, Type defaultType) where T : MemberInfo
        {
            Assumes.NotNull(cache);

            int metadataToken = cache.ReadValue<int>(AttributedCacheServices.CacheKeys.MetadataToken);
            Assembly assembly = cache.LoadAssembly(defaultType == null ? null : defaultType.Assembly);
            Module module = cache.LoadModule(assembly);

            return (T)ReflectionResolver.ResolveMember(module, metadataToken);
        }

        private static ParameterInfo LoadParameterCore(IDictionary<string, object> cache, ConstructorInfo constructor)
        {
            Assumes.NotNull(constructor);
            
            int parameterPosition = cache.ReadValue<int>(AttributedCacheServices.CacheKeys.ParameterPosition);

            Assumes.IsTrue(parameterPosition >= 0);

            ParameterInfo[] parameters = constructor.GetParameters();

            Assumes.IsTrue(parameterPosition < parameters.Length);

            return parameters[parameterPosition];
        }

        private static void Write<T>(this IDictionary<string, object> cache, Action<IDictionary<string, object>, T> writer, string key, T value)
        {
            Write<T>(cache, writer, key, value, default(T));
        }

        private static void Write<T>(this IDictionary<string, object> cache, Action<IDictionary<string, object>, T> writer, string key, T value, T defaultValue)
        {
            if (object.Equals(value, defaultValue))
            {   // Only write out the value if it is not the default
                return;
            }

            var valueCache = new Dictionary<string, object>();
            writer(valueCache, value);

            cache.WriteDictionary(key, valueCache);
        }

        private static T Load<T>(this IDictionary<string, object> cache, Func<IDictionary<string, object>, T> loader, string key)
        {
            return Load(cache, loader, key, default(T));
        }

        private static T Load<T>(this IDictionary<string, object> cache, Func<IDictionary<string, object>, T> loader, string key, T defaultValue)
        {
            var valueCache = cache.ReadDictionary<object>(key);
            if (valueCache.Count != 0)
            {
                return loader(valueCache);
            }

            return defaultValue;
        }
    }
}